cmake_minimum_required(VERSION 3.14)
project(Halide)

set(CPACK_PACKAGE_VENDOR "Halide")
set(CPACK_RESOURCE_FILE_LICENSE "${Halide_SOURCE_DIR}/LICENSE.txt")
set(CPACK_MONOLITHIC_INSTALL OFF)
if (WIN32)
  set(CPACK_GENERATOR "ZIP")
else()
  set(CPACK_GENERATOR "TGZ")
endif()
# Remove this to get package names that are formatted as
# ${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CPACK_SYSTEM_NAME}.
set(CPACK_PACKAGE_FILE_NAME "Halide" CACHE STRING "Name of package created by distrib target")
include(CPack)

find_package(Threads QUIET)

if("${HALIDE_REQUIRE_LLVM_VERSION}" STREQUAL "")
  # Find any version present.
  find_package(LLVM REQUIRED CONFIG)
else()
  # Find a specific version.
  string(REGEX REPLACE
    "^([0-9]+)([0-9])$"
    "\\1"
    MAJOR
    "${HALIDE_REQUIRE_LLVM_VERSION}")
  string(REGEX REPLACE
    "^([0-9]+)([0-9])$"
    "\\2"
    MINOR
    "${HALIDE_REQUIRE_LLVM_VERSION}")
  message("HALIDE_REQUIRE_LLVM_VERSION ${HALIDE_REQUIRE_LLVM_VERSION}")
  message("Looking for LLVM version ${MAJOR}.${MINOR}")
  find_package(LLVM "${MAJOR}.${MINOR}" REQUIRED CONFIG)
  if(NOT "${LLVM_VERSION_MAJOR}${LLVM_VERSION_MINOR}" STREQUAL "${MAJOR}${MINOR}")
    message(FATAL_ERROR "LLVM version error: required ${MAJOR}${MINOR} but found ${LLVM_VERSION_MAJOR}${LLVM_VERSION_MINOR}")
  endif()
endif()

# Notify the user what paths and LLVM version we are using
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")

if (MSVC AND NOT CMAKE_CONFIGURATION_TYPES)
  # LLVM5.x on Windows can include "$(Configuration)" in the path;
  # fix this so we can use the paths right away.
  string(REPLACE "$(Configuration)" "${CMAKE_BUILD_TYPE}" LLVM_TOOLS_BINARY_DIR "${LLVM_TOOLS_BINARY_DIR}")
endif()

set_property(GLOBAL PROPERTY USE_FOLDERS ON)

if(${CMAKE_SYSTEM_NAME} MATCHES "Windows")
  set(CMAKE_OBJECT_PATH_MAX 260)
  message("Windows: setting CMAKE_OBJECT_PATH_MAX to ${CMAKE_OBJECT_PATH_MAX}")
endif()

set(CMAKE_MACOSX_RPATH ON)

# Export all symbols
SET(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")

set(LLVM_VERSION "${LLVM_VERSION_MAJOR}${LLVM_VERSION_MINOR}")

file(TO_NATIVE_PATH "${LLVM_TOOLS_BINARY_DIR}/llvm-as${CMAKE_EXECUTABLE_SUFFIX}" LLVM_AS)
file(TO_NATIVE_PATH "${LLVM_TOOLS_BINARY_DIR}/llvm-nm${CMAKE_EXECUTABLE_SUFFIX}" LLVM_NM)
file(TO_NATIVE_PATH "${LLVM_TOOLS_BINARY_DIR}/clang${CMAKE_EXECUTABLE_SUFFIX}" CLANG)
file(TO_NATIVE_PATH "${LLVM_TOOLS_BINARY_DIR}/llvm-config${CMAKE_EXECUTABLE_SUFFIX}" LLVM_CONFIG)

if(LLVM_USE_SHARED_LLVM_LIBRARY)
  # Since we will be linking to the shared LLVM library,
  # we will get all the transitive dependencies for free automagically.
  set(HALIDE_SYSTEM_LIBS)
else()
  # LLVM doesn't appear to expose --system-libs via its CMake interface,
  # so we must shell out to llvm-config to find this info
  execute_process(COMMAND ${LLVM_CONFIG} --system-libs --link-static OUTPUT_VARIABLE HALIDE_SYSTEM_LIBS_RAW)
  string(STRIP "${HALIDE_SYSTEM_LIBS_RAW}" HALIDE_SYSTEM_LIBS_RAW)  # strip whitespace from start & end
  string(REPLACE " " ";" HALIDE_SYSTEM_LIBS "${HALIDE_SYSTEM_LIBS_RAW}")  # convert into a list
  if("${HALIDE_SYSTEM_LIBS}" STREQUAL "")
    # It's theoretically possible that this could be legitimately empty,
    # but in practice that doesn't really happen, so we'll assume it means we
    # aren't configured correctly.
    message(WARNING "'llvm-config --system-libs --link-static' is empty; this is possibly wrong.")
  endif()
endif()

execute_process(COMMAND ${LLVM_CONFIG} --cxxflags OUTPUT_VARIABLE LLVM_CONFIG_CXXFLAGS)
string(FIND "${LLVM_CONFIG_CXXFLAGS}" "-std=c++2a" LLVM_CPP2a)
string(FIND "${LLVM_CONFIG_CXXFLAGS}" "-std=c++20" LLVM_CPP20)
string(FIND "${LLVM_CONFIG_CXXFLAGS}" "-std=c++17" LLVM_CPP17)
string(FIND "${LLVM_CONFIG_CXXFLAGS}" "-std=c++14" LLVM_CPP14)
if (LLVM_CPP2a GREATER -1 OR LLVM_CPP20 GREATER -1)
  set(CMAKE_CXX_STANDARD 20)
elseif (LLVM_CPP17 GREATER -1)
  set(CMAKE_CXX_STANDARD 17)
elseif (LLVM_CPP14 GREATER -1)
  set(CMAKE_CXX_STANDARD 14)
else()
  # Require (at least) C++11 for everything.
  set(CMAKE_CXX_STANDARD 11)
endif()

string(FIND "${LLVM_CONFIG_CXXFLAGS}" "-stdlib=libc++" LLVM_LIBCXX)
if (LLVM_LIBCXX GREATER -1)
  add_compile_options(-stdlib=libc++)
  if(COMMAND add_link_options)
    add_link_options(-stdlib=libc++)
  else()
    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -stdlib=libc++")
    set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -stdlib=libc++")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++")
  endif()
endif()

set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Check LLVM
function(check_dir VARNAME PATH)
  if (NOT IS_ABSOLUTE "${PATH}")
    message(FATAL_ERROR "\"${PATH}\" (${VARNAME}) must be an absolute path")
  endif()
  if (NOT IS_DIRECTORY "${PATH}")
    message(FATAL_ERROR "\"${PATH}\" (${VARNAME}) must be a directory")
  endif()
endfunction()
function(check_tool_exists NAME PATH)
  # Need to convert to CMake path so that backslashes don't get
  # interpreted as an escape.
  file(TO_CMAKE_PATH "${PATH}" TOOL_PATH)
  if (MSVC)
    # LLVM5.x on Windows can include "$(Configuration)" in the path;
    # fix this so we can use the paths right away.
    string(REPLACE "$(Configuration)" "${CMAKE_BUILD_TYPE}" TOOL_PATH "${TOOL_PATH}")
  endif()
  if (NOT EXISTS "${TOOL_PATH}")
    message(FATAL_ERROR "Tool ${NAME} not found at ${TOOL_PATH}")
  endif()
  message(STATUS "Using ${NAME} at ${TOOL_PATH}")
endfunction()

# Check LLVM tools exist
check_tool_exists(llvm-as "${LLVM_AS}")
check_tool_exists(llvm-nm "${LLVM_NM}")
check_tool_exists(clang "${CLANG}")

# Check reported LLVM version
if (NOT "${LLVM_VERSION}" MATCHES "^[0-9]+$")
  message(FATAL_ERROR "LLVM_VERSION not specified correctly, must be LLVM version times 10.")
endif()
if (LLVM_VERSION LESS 80)
  message(FATAL_ERROR "LLVM version must be 8.0 or newer")
endif()
if (LLVM_VERSION GREATER 110)
  message(FATAL_ERROR "LLVM version must be 11.0 or older")
endif()

function(check_llvm_target TARGET HAS_TARGET)
  set(${HAS_TARGET} OFF PARENT_SCOPE)
  set(_llvm_required_version ${LLVM_VERSION})
  if (ARGV2)
    set(_llvm_required_version ${ARGV2})
  endif()
  if (NOT LLVM_VERSION LESS _llvm_required_version)
    list(FIND LLVM_TARGETS_TO_BUILD ${TARGET} _found_target)
    if (_found_target GREATER -1)
      set(${HAS_TARGET} ON PARENT_SCOPE)
    else()
      set(${HAS_TARGET} OFF PARENT_SCOPE)
    endif()
  endif()
endfunction()

check_llvm_target(X86 WITH_X86)
check_llvm_target(ARM WITH_ARM)
check_llvm_target(AArch64 WITH_AARCH64)
check_llvm_target(Hexagon WITH_HEXAGON 40)
check_llvm_target(Mips WITH_MIPS)
check_llvm_target(PowerPC WITH_POWERPC)
check_llvm_target(NVPTX WITH_NVPTX)
check_llvm_target(RISCV WITH_RISCV)
# AMDGPU target is WIP
check_llvm_target(AMDGPU WITH_AMDGPU)

option(TARGET_X86 "Include x86 target" ${WITH_X86})
option(TARGET_ARM "Include ARM target" ${WITH_ARM})
option(TARGET_AARCH64 "Include AARCH64 (arm64) target" ${WITH_AARCH64})
option(TARGET_HEXAGON "Include Hexagon target" ${WITH_HEXAGON})
option(TARGET_METAL "Include Metal target" ON)
option(TARGET_MIPS "Include MIPS target" ${WITH_MIPS})
option(TARGET_POWERPC "Include POWERPC target" ${WITH_POWERPC})
option(TARGET_PTX "Include PTX target" ${WITH_NVPTX})
option(TARGET_AMDGPU "Include AMDGPU target" ${WITH_AMDGPU})
option(TARGET_RISCV "Include RISCV target" ${WITH_RISCV})
option(TARGET_OPENCL "Include OpenCL-C target" ON)
option(TARGET_OPENGL "Include OpenGL/GLSL target" ON)
option(TARGET_OPENGLCOMPUTE "Include OpenGLCompute target" ON)
option(TARGET_D3D12COMPUTE "Include Direct3D 12 Compute target" ON)
option(HALIDE_SHARED_LIBRARY "Build as a shared library" ON)
option(LLVM_USE_SHARED_LLVM_LIBRARY "Use shared versions of LLVM libraries" OFF)
option(HALIDE_ENABLE_RTTI "Enable RTTI" ${LLVM_ENABLE_RTTI})
option(HALIDE_ENABLE_EXCEPTIONS "Enable exceptions" ${LLVM_ENABLE_EH})
option(HALIDE_USE_CODEMODEL_LARGE "Use the Large LLVM codemodel" OFF)

if (HALIDE_SHARED_LIBRARY)
  set(HALIDE_LIBRARY_TYPE SHARED)
  message(STATUS "Building Halide as a shared library")
else()
  set(HALIDE_LIBRARY_TYPE STATIC)
  message(STATUS "Building Halide as a static library")
endif()

if (HALIDE_ENABLE_RTTI AND NOT LLVM_ENABLE_RTTI)
  message(FATAL_ERROR "Can't enable RTTI. LLVM was compiled without it")
endif()

# Needed for 'make distrib' to properly fill in the .tpl files
if (HALIDE_ENABLE_RTTI)
  set(HALIDE_RTTI_RAW 1)
else()
  set(HALIDE_RTTI_RAW 0)
endif()

function(halide_project name folder)
  add_executable("${name}" ${ARGN})
  if (MSVC)
    if(NOT HALIDE_ENABLE_RTTI)
      target_compile_options("${name}" PUBLIC $<$<COMPILE_LANGUAGE:CXX>:/GR- >)
    endif()
  else()
    if (NOT HALIDE_ENABLE_RTTI)
      target_compile_options("${name}" PUBLIC $<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>)
    endif()
  endif()
  target_link_libraries("${name}" PRIVATE ${HALIDE_COMPILER_LIB} ${CMAKE_DL_LIBS} ${CMAKE_THREAD_LIBS_INIT})
  target_include_directories("${name}" PRIVATE "${Halide_SOURCE_DIR}/src")
  target_include_directories("${name}" PRIVATE "${Halide_SOURCE_DIR}/tools")
  set_target_properties("${name}" PROPERTIES
          FOLDER "${folder}"
          ENABLE_EXPORTS True)
  if (MSVC)
    target_link_libraries("${name}" PRIVATE Kernel32)
  endif()
endfunction(halide_project)

# Set warnings globally
option(WARNINGS_AS_ERRORS "Treat warnings as errors" ON)
if (WARNINGS_AS_ERRORS)
    message(STATUS "WARNINGS_AS_ERRORS enabled")
else()
    message(STATUS "WARNINGS_AS_ERRORS disabled")
endif()

if (NOT MSVC)
  add_compile_options(
          -Wall
          -Wno-unused-function
          -Wcast-qual
          -Wignored-qualifiers
          $<$<COMPILE_LANGUAGE:CXX>:-Woverloaded-virtual>
          $<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CXX_COMPILER_ID:GNU>,$<VERSION_GREATER:$<CXX_COMPILER_VERSION>,5.1>>:-Wsuggest-override>
  )
  if (WARNINGS_AS_ERRORS)
    add_compile_options(-Werror)
  endif()
  if (HALIDE_ENABLE_EXCEPTIONS)
    add_compile_options(-DWITH_EXCEPTIONS)
  endif()
  if (HALIDE_USE_CODEMODEL_LARGE)
    add_compile_options(-DHALIDE_USE_CODEMODEL_LARGE)
  endif()
else()
  add_compile_options(/W3)
  add_compile_options(/wd4018)  # disable "signed/unsigned mismatch"
  add_compile_options(/wd4503)  # disable "decorated name length exceeded, name was truncated"
  add_compile_options(/wd4267)  # disable "conversion from 'size_t' to 'int', possible loss of data"
  add_compile_options(/wd4800)  # forcing value to bool 'true' or 'false' (performance warning)
  if (WARNINGS_AS_ERRORS)
    add_compile_options(/WX)
  endif()
  if (HALIDE_ENABLE_EXCEPTIONS)
    add_compile_options(/DWITH_EXCEPTIONS)
  endif()
  if (HALIDE_USE_CODEMODEL_LARGE)
    add_compile_options(/DHALIDE_USE_CODEMODEL_LARGE)
  endif()
endif()

# These tools are needed by several subdirectories
add_executable(build_halide_h tools/build_halide_h.cpp)
add_executable(binary2cpp tools/binary2cpp.cpp)
if (MSVC)
  # disable irrelevant "POSIX name" warnings
  target_compile_options(build_halide_h PUBLIC /wd4996)
  target_compile_options(binary2cpp PUBLIC /wd4996)
endif()


# Look for OpenMP
find_package(OpenMP QUIET)
if (OPENMP_FOUND)
  message(STATUS "Found OpenMP")
endif()

# For in-tree builds, we need to set the input variables for halide.cmake
# to specific values, rather than relying on HALIDE_DISTRIB_DIR to be set correctly.
set(HALIDE_INCLUDE_DIR "${CMAKE_BINARY_DIR}/include")
set(HALIDE_TOOLS_DIR "${Halide_SOURCE_DIR}/tools")
set(HALIDE_COMPILER_LIB Halide)
set(HALIDE_DISTRIB_DIR "/bad-path")
include(halide.cmake)

# -----------------------------------------------------------------------------
# Option to enable/disable assertions
# -----------------------------------------------------------------------------
# Filter out definition of NDEBUG definition from the default build
# configuration flags.  # We will add this ourselves if we want to disable
# assertions.
# FIXME: Perhaps our own default ``cxx_flags_overrides.cmake`` file would be better?
foreach (build_config Debug Release RelWithDebInfo MinSizeRel)
  string(TOUPPER ${build_config} upper_case_build_config)
  foreach (language CXX C)
    set(VAR_TO_MODIFY "CMAKE_${language}_FLAGS_${upper_case_build_config}")
    string(REGEX REPLACE "(^| )[/-]D *NDEBUG($| )"
                         " "
                         replacement
                         "${${VAR_TO_MODIFY}}"
          )
    #message("Original (${VAR_TO_MODIFY}) is ${${VAR_TO_MODIFY}} replacement is ${replacement}")
    set(${VAR_TO_MODIFY} "${replacement}" CACHE STRING "Default flags for ${build_config} configuration" FORCE)
  endforeach()
endforeach()

# TODO: this is intended to eventually replicate all of the interesting test targets
#       from our Make build, but not all are implemented yet:
# TODO(srj): add test_aotcpp_generators support
# TODO(srj): add test_valgrind variant
# TODO(srj): add test_avx512 variant
# TODO(srj): add test_python variant
# TODO(srj): add test_apps variant
function(add_halide_test TARGET)
  set(options EXPECT_FAILURE)
  set(oneValueArgs WORKING_DIRECTORY)
  set(multiValueArgs GROUPS)
  cmake_parse_arguments(args "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

  add_test(NAME ${TARGET}
          COMMAND ${TARGET}
          WORKING_DIRECTORY "${args_WORKING_DIRECTORY}")

  set_tests_properties(${TARGET} PROPERTIES LABELS "${args_GROUPS}")
  if (${args_EXPECT_FAILURE})
    set_tests_properties(${TARGET} PROPERTIES WILL_FAIL true)
  endif ()
endfunction()

add_subdirectory(src)
option(WITH_TESTS "Build tests" ON)
if (WITH_TESTS)
  message(STATUS "Building tests enabled")
  enable_testing()
  add_subdirectory(test)
else()
  message(STATUS "Building tests disabled")
endif()

option(WITH_APPS "Build apps" ON)
if (WITH_APPS)
  message(STATUS "Building apps enabled")
  add_subdirectory(apps)
else()
  message(STATUS "Building apps disabled")
endif()

option(WITH_TUTORIALS "Build Tutorials" ON)
if (WITH_TUTORIALS)
  message(STATUS "Building tutorials enabled")
  add_subdirectory(tutorial)
else()
  message(STATUS "Building tutorials disabled")
endif()

option(WITH_DOCS "Enable building of documentation" OFF)
if (WITH_DOCS)
find_package(Doxygen)
  if (NOT DOXYGEN_FOUND)
    message(FATAL_ERROR "Could not find Doxygen. Either install it or set WITH_DOCS to OFF")
  endif()

  configure_file(${Halide_SOURCE_DIR}/Doxyfile.in ${CMAKE_BINARY_DIR}/Doxyfile @ONLY)
  # Note documentation is not built by default, the user needs to build the "doc" target
  add_custom_target(doc
    COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_BINARY_DIR}/Doxyfile
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
    COMMENT "Building Doxygen documentation"
  )
endif()

option(WITH_UTILS "Build utils" ON)
if (WITH_UTILS)
  message(STATUS "Building utils enabled")
  add_subdirectory(util)
else()
  message(STATUS "Building utils disabled")
endif()

# ------------------------------------------------
# install

# TODO: the use of glob here is almost certainly brittle if not outright wrong.
file(GLOB FILES "${CMAKE_BINARY_DIR}/include/HalideRuntime*.h")
install(FILES
        "${CMAKE_BINARY_DIR}/include/Halide.h"
        "${CMAKE_BINARY_DIR}/include/HalideBuffer.h"
        "${CMAKE_BINARY_DIR}/include/HalidePyTorchHelpers.h"
        "${CMAKE_BINARY_DIR}/include/HalidePyTorchCudaHelpers.h"
        ${FILES}
        DESTINATION include)

install(DIRECTORY tutorial
        DESTINATION .
        FILES_MATCHING
        PATTERN "*.cpp"
        PATTERN "*.h"
        PATTERN "lesson_*.sh"
        PATTERN "*.gif"
        PATTERN "*.jpg"
        PATTERN "*.mp4"
        PATTERN "*.png")

# ---- Tools
install(FILES
        tools/mex_halide.m
        tools/GenGen.cpp
        tools/RunGen.h
        tools/RunGenMain.cpp
        tools/halide_benchmark.h
        tools/halide_image.h
        tools/halide_image_io.h
        tools/halide_image_info.h
        tools/halide_malloc_trace.h
        tools/halide_trace_config.h
        DESTINATION tools)

# ---- README
install(FILES
        CODE_OF_CONDUCT.md
        README_cmake.md
        README.md
        README_rungen.md
        README_webassembly.md
        DESTINATION .)

# ---- halide.cmake
install(FILES halide.cmake DESTINATION .)

# ---- halide_config
file(GLOB FILES "${Halide_SOURCE_DIR}/tools/halide_config.*.tpl")
foreach(F ${FILES})
  get_filename_component(FNAME "${F}" NAME)  # Extract filename
  string(REGEX REPLACE "\\.tpl$" "" FNAME "${FNAME}")  # Strip .tpl extension
  configure_file("${F}" "${CMAKE_BINARY_DIR}/${FNAME}" @ONLY)
  install(FILES "${CMAKE_BINARY_DIR}/${FNAME}"
          DESTINATION .)
endforeach()

add_custom_target(distrib
  COMMAND ${CMAKE_COMMAND} -E echo "\\'make distrib\\' is not available under CMake. Use \\'make package\\' instead.")

add_custom_target(format
  COMMAND find "${Halide_SOURCE_DIR}/apps" "${Halide_SOURCE_DIR}/src" "${Halide_SOURCE_DIR}/tools" "${Halide_SOURCE_DIR}/test" "${Halide_SOURCE_DIR}/util" "${Halide_SOURCE_DIR}/python_bindings" -name *.cpp -o -name *.h -o -name *.c | xargs ${CLANG}-format -i -style=file)
